extern crate chrono;
use colored::Colorize;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use regex::Regex;
use reqwest;
use reqwest::RedirectPolicy;
use std::error::Error;
use std::fs::{create_dir_all, metadata, remove_file, File};
use std::io::{Read, Write};
use std::path::Path;
use std::sync::Mutex;
use std::fs::OpenOptions;
use std::thread;
use std::time;
// use std::io::SeekFrom;
// use std::io::Seek;
// use regex::Regex;
// use std::io::Seek;
use glob::glob;
// use std::io::SeekFrom;
use dirs;
use log::{error, info};
// use std::fs::OpenOptions;

// use chrono::offset::Utc;
// use chrono::DateTime;
// use http::header::HeaderName;
// use std::time::SystemTime;

const SIZE: usize = 1048576;
// use percent_encoding::percent_decode;

// use std::sync::mpsc::{Receiver,Sender};
// use std::sync::mpsc::channel;

pub type Except<T> = Result<T, Box<dyn Error>>;
pub trait PathHome {
    fn glob(&self) -> String;
}

impl<'a> PathHome for &'a str {
    fn glob(&self) -> String {
        self.to_string().glob()
    }
}

impl PathHome for String {
    fn glob(&self) -> String {
        if self.starts_with("~/") {
            if let Some(home) = dirs::home_dir() {
                home.join(&self[2..]).to_str().unwrap().to_string()
            } else {
                self.to_string()
            }
        } else {
            for e in glob(self).unwrap() {
                match e {
                    Ok(a) => {
                        return a.to_str().unwrap().to_string();
                    }
                    _ => {}
                }
            }
            self.to_string()
        }
    }
}

struct TMP {
    dir: String,
    // pool:threadpool::ThreadPool,
    // thread_num: i32,
}
impl<'a> TMP {
    fn get_path(&self) -> String {
        self.dir.clone()
    }
}

lazy_static! {
    static ref PROGRESSBAR: Mutex<MultiProgress> = {
        let m = MultiProgress::new();
        Mutex::new(m)
    };
    static ref RES: Mutex<TMP> = {
        Mutex::new(TMP {
            dir: "/tmp/".to_string(),
        })
    };
}

// type BLOCK = [0;SIZE];

struct Block {
    start: u64,
    end: u64,
    path: String,
    no: i32,
}

impl Clone for Block{
    fn clone(&self) -> Self{
        Self{
            start:self.start,
            end: self.end,
            path: self.path.clone(),
            no: self.no
        }
    }
}

impl Block {
    // add code here
    fn new(path: &str) -> Except<Self> {
        if !Path::new(path).exists() {
            return Err("no such file to match".into());
        }
        if !path.contains("-") {
            return Err("no valid file to match".into());
        }
        let re = Regex::new(r#"(\d+)\-(\d+)\.(\d+)$"#).unwrap();
        if !re.is_match(path) {
            return Err("no valid file to match".into());
        }

        let name = Path::new(path).file_name().unwrap().to_str().unwrap();
        let caps = re.captures(name).unwrap();
        let start = caps
            .get(1)
            .map_or("", |m| m.as_str())
            .parse::<u64>()
            .unwrap();
        let end = caps
            .get(2)
            .map_or("", |m| m.as_str())
            .parse::<u64>()
            .unwrap();
        let no = caps
            .get(3)
            .map_or("", |m| m.as_str())
            .parse::<i32>()
            .unwrap();
        Ok(Self {
            start: start,
            end: end,
            path: path.to_string(),
            no: no,
        })
    }
    fn remove_file(&self) {
        remove_file(&self.path).unwrap();
    }

    pub fn get_name(&self) -> String {
        Path::new(&self.path)
            .file_name()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string()
    }

    pub fn check_if_valid(&self) -> bool {
        let l = metadata(&self.path).unwrap().len();
        if l - 1 != self.end - self.start {
            info!(
                "l:{}  file : {} | {} ",
                l,
                &self.get_name().yellow(),
                self.get_no()
            );
            false
        } else {
            true
        }
    }

    pub fn get_no(&self) -> i32 {
        self.no
    }

    pub fn get_raw_name(&self) -> String {
        let re = Regex::new(r#"(\.\d+\-\d+\.\d+)"#).unwrap();
        re.replace_all(&self.path, "").to_string()
    }

    pub fn search_block(target_path: &str) -> Option<Block> {
        let pattern = format!("{}.*.*", target_path);
        for e in glob(&pattern).expect("no file to read") {
            match e {
                Ok(path) => {
                    let b = match Block::new(path.to_str().unwrap()) {
                        Ok(bb) => bb,
                        Err(_) => continue,
                    };
                    if b.check_if_valid() {
                        return Some(b);
                    } else {
                        // remove_file(&path).unwrap();
                        info!(
                            "file is not valid | remove {}",
                            path.to_str().unwrap().to_string().red()
                        );
                    }
                }
                Err(e) => println!("{:?}", e),
            }
        }
        None
    }
    // pub fn last_block(&self) -> Option<Block>{
    //     if self.no == 0{
    //         None
    //     }else{
    //         return self.get_block_by_no(self.no -1 );
    //     }
    // }

    // pub fn next_block(&self) -> Option<Block>{
    //     if self.no == 0{
    //         None
    //     }else{
    //         return self.get_block_by_no(self.no + 1);
    //     }
    // }

    pub fn merge(&self) -> u64 {
        let raw = self.get_raw_name();
        let raw_path = Path::new(&raw);
        let now_size = metadata(&raw_path).unwrap().len();
        // if now_size < self.end &&  now_size + 1 >= self.start{
        // info!("merge with {}-{} ===> {}", self.start, self.end, now_size);
        let mut reader = File::open(&self.path).unwrap();
        let mut writer = OpenOptions::new()
            .append(true)
            .open(&raw)
            .expect("open file failed(a+)");
        // reader.seek(SeekFrom::Start(now_size - self.start)).expect("seek failed");
        let pro = new_pro(self.end - now_size + 1);
        match copy_with_progress(
            &format!("merege continu:{}", now_size + 1),
            &mut reader,
            &mut writer,
            &pro,
            None
        ) {
            Ok(_) => {}
            Err(_) => {
                error!(
                    "merge failed {} -> {} ",
                    self.get_name(),
                    self.get_raw_name()
                );
            }
        };
        metadata(&raw_path).unwrap().len()
        // info!("merge with {}-{} ===> {} [{}]", self.start, self.end, now_size, "ok".green());
        // }else{
        // 0
        // }
    }

    pub fn get_block_by_no(&self, no: i32) -> Option<Block> {
        let end = format!("{}.*-*.{}", &self.get_raw_name(), no);
        for e in glob(&end).expect("no file to read") {
            match e {
                Ok(path) => {
                    let bb = match Block::new(path.to_str().unwrap()) {
                        Ok(b) => b,
                        _ => return None,
                    };
                    return Some(bb);
                }
                _ => {}
            };
        }
        None
    }

    pub fn get_raw_size(&self) -> Except<u64> {
        let raw = self.get_raw_name();
        if !Path::new(&raw).exists() {
            File::create(&raw)?;
            Ok(0)
        } else {
            Ok(metadata(&raw)?.len())
        }
    }

    pub fn all_merge(target_path: &str) -> Except<u64> {
        if let Some(block) = Block::search_block(target_path) {
            let mut count = 0;
            let _ = block.get_raw_size();
            let mut last_b :Block = block.clone();
            loop {
                if let Some(fb) = block.get_block_by_no(count) {
                    fb.merge();
                    last_b = fb;
                    count += 1;
                } else {
                    log::info!("not found : {}", count);
                    break;
                }
            }
            let size_re = Regex::new(r#"\-(\d+)\.\d+$"#).unwrap();
            let now_size = block.get_raw_size().unwrap();
            let res:usize = size_re.captures(&last_b.path).unwrap().get(1).map_or("",|x|x.as_str()).parse::<usize>().unwrap();
            info!("last : size: {}", res);
            if now_size >= (res) as u64{
                // if  let Ok(f) = std::fs::OpenOptions::new().truncate(true).open(target_path){
                //     // let _ = f.set_len((res-1) as u64);
                // };
                if let Some(block) = Block::search_block(target_path) {
                    let mut count = 0;
                    // let _ = block.get_raw_size();
                    loop {
                        if let Some(fb) = block.get_block_by_no(count) {
                            fb.remove_file();
                            count += 1;
                        } else {
                            log::info!("remove not found : {}", count);
                            break;
                        }
                    }
                    // block.get_raw_size()
                }
            }else{
                let pp = last_b.get_raw_name();
                let p  = Path::new(&pp);
                if p.exists(){
                    let _ = remove_file(p);
                }
            }
            
            block.get_raw_size()
            
        } else {
            Ok(0)
        }
    }
}

pub struct DownloadObject {
    url: String,
    len: u64,
    start: u64,
    blocks: u64,
    unit: String,
    support_mu: bool,
    thread_num: i32,
    // recv:Receiver<String>,
    // send:Sender<String>,
    dst: String,
}

impl Default for DownloadObject {
    fn default() -> Self {
        // let (tx,rx) = channel();
        Self {
            url: "http://localhost:8080".to_string(),
            len: 0,
            start: 0 as u64,
            blocks: 0 as u64,
            unit: "".to_string(),
            support_mu: false,
            // send:tx,
            // recv:rx,
            thread_num: 10,
            dst: "/tmp/tmp.save.bin".to_string(),
        }
    }
}

impl DownloadObject {
    pub fn new(url: &str, num: i32) -> Except<Self> {
        // let mut client = reqwest::Client::new();
        let client = reqwest::Client::builder()
            .danger_accept_invalid_certs(true)
            .redirect(RedirectPolicy::custom(|attempt| {
                let url = attempt.url().as_str();
                log::info!("-> follow -> {}", url.bold().underline().green());
                attempt.follow()
            }))
            .build()
            .unwrap();
        let test_header_resp = client.head(url).send()?;
        let mut obj = DownloadObject {
            url: url.to_string(),
            thread_num: num,
            ..DownloadObject::default()
        };
        let tmp_dir_string = get_tmp_dir();
        let tmp_dir = Path::new(tmp_dir_string.as_str());
        let file_name = url.split("/").last().unwrap();
        let dst = tmp_dir.join(file_name).to_str().unwrap().to_string();
        // info!("test if multi {}  [{}]", url.yellow(), "wait".blue());

        if test_header_resp.status().is_success() {
            if let Some(length_v) = test_header_resp.headers().get("Content-Length") {
                obj.len = length_v.to_str()?.parse::<u64>()?;
                if let Some(d) = test_header_resp.headers().get("Accept-Ranges") {
                    if d.to_str()?.trim() != "none" {
                        obj.unit = d.to_str()?.trim().to_string();
                        obj.blocks = (obj.len / SIZE as u64) + 1;
                        obj.support_mu = true;
                        obj.dst = dst;
                        info!("init multi-down| size: {}   [{}]", obj.len, "ok".green());
                    }
                    Ok(obj)
                } else {
                    info!(
                        "init multi-down| size: {}   [{}]",
                        obj.len,
                        "single".green()
                    );
                    Ok(obj)
                }
            } else {
                Err("no content length to capture".into())
            }
        } else {
            log::info!("status : {:?}", test_header_resp.status());
            if let Some(length_v) = test_header_resp.headers().get("Content-Length") {
                obj.len = length_v.to_str()?.parse::<u64>()?;
                if let Some(d) = test_header_resp.headers().get("Accept-Ranges") {
                    if d.to_str()?.trim() != "none" {
                        obj.unit = d.to_str()?.trim().to_string();
                        obj.blocks = (obj.len / SIZE as u64) + 1;
                        obj.support_mu = true;
                        obj.dst = dst;
                        info!("init multi-down| size: {}   [{}]", obj.len, "ok".green());
                    }
                    Ok(obj)
                } else {
                    info!(
                        "init multi-down| size: {}   [{}]",
                        obj.len,
                        "single".green()
                    );
                    Ok(obj)
                }
            } else {
                let headers = test_header_resp.headers();
                for (name, value) in headers {
                    log::info!("{} : {}", name.as_str(), value.to_str().unwrap().bold().underline());
                }

                Err("no content length to capture".into())
            }
        }
    }
    fn range(&self, start: u64, end: u64) -> String {
        format!("{}={}-{}", &self.unit, start, end)
    }

    // fn get_finished_file(&self) -> Vec<String>{
    //     let target_file = self.get_dst();
    //     let path = Path::new(&target_file);
    //     let parent = path.parent().expect("no parent!!");

    //     finish
    // }

    fn get_dst(&self) -> String {
        self.dst.clone()
    }

    // pub fn downloaded(&self, pro:&ProgressBar, all:i32){
    //     let name = self.get_dst();
    //     let file_path = Path::new(&name);
    //     // let mut recv_num:Vec<i32> = vec![];
    //     let mut file = File::create(file_path).expect("creat file error");
    //     // let mut finish_count = 0;
    //     // let mut wait_str:String = String::from("");
    //     for no in 0..all{
    //         let mut rfile = File::open(format!("{}.{}", &name, no)).expect("open failed");
    //         copy_with_progress(&format!("Merge {}",no),&mut rfile, &mut file, pro).expect("copy failed");
    //         remove_file(&format!("{}.{}", &name, no)).expect("no such file?");
    //     }

    // }
    pub fn downloaded(&self) {
        let size = Block::all_merge(&self.get_dst()).expect("merge failed ?");
        let finish = format!(
            "{:.3}/{:.3}M",
            size as f64 / 1024.0 / 1024.0,
            self.len as f64 / 1024.0 / 1024.0
        );
        info!("Download : {} m", &finish.green());
    }

    pub fn download(&self) {
        // let size = self.len / self.thread_num as u64;
        let threadpool = threadpool::ThreadPool::new(self.thread_num as usize);
        let mut no = 0;
        let mut start = self.start;
        let file_path = self.get_dst();
        if Path::new(&file_path).exists() {
            start = metadata(&file_path).expect("no meta").len();
            info!(
                "File continue : {} | start at :{}",
                file_path.green(),
                &format!("{}", start).blue()
            );
            if start == self.len {
                info!("finished {}", file_path.green());
                return;
            }
        }
        // let start = metadata(&file_path).expect("no meta").len();
        let size = (self.len - start) / self.thread_num as u64;
        let m = MultiProgress::new();
        let sty = ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes:.yellow}/{total_bytes} {bytes_per_sec:.bold.green} ({eta}) {msg}")
            .progress_chars("#>-");

        loop {
            let mut is_end  = false;
            let mut end = start + size;
            if end > self.len {
                end = self.len;
                is_end = true;
            }
            
            let url = self.url.clone();
            let header = self.range(start, end);
            let dst = file_path.clone();
            let pro_bar = m.add(ProgressBar::new(size));
            pro_bar.set_style(sty.clone());
            let all_need_size = self.len;
            // let tx = self.send.clone();
            threadpool.execute(move || {
                let mut finished_f:bool = false;
                let is_end = is_end;
                // let this_file_start = start;
                // let this_file_end = end;
                let mut try_count = 0;
                'try_down:
                loop{
                    // println!("DEBUG {}",no);
                    match download_at(&url, &dst, no, &header, &pro_bar, all_need_size) {
                        Ok(need_size) => {
                            // info!("no.{} :{}",no,need_size);
                            let _size = header.split("=").last().expect("no last");
                            let _proname = header.split("=").next().expect("no = in header");
                            let path = format!("{}.{}.{}", &dst, _size, no);
                            let now_size = metadata(&path).expect("no such file to stat").len();
                            if now_size == need_size -1 && is_end{
                                finished_f = true;
                            }
                            if now_size >= need_size {
                                finished_f = true;
                            }
                            
                            
                        }
                        Err(e) => {
                            // log::error!("{}", e);
                            pro_bar.println(format!("reason {} | {}",e,&format!("again no.{} {}",no, try_count).green()));
                            pro_bar.set_message("break wait try again");
                            try_count += 1
                        }
                    };
                    if finished_f{
                        
                        break 'try_down
                    }
                    pro_bar.set_message(&format!("try time: {}", try_count));
                }
                
                let _ = thread::sleep(time::Duration::from_secs(1));
                
            });
            no += 1;
            start = end + 1;
            if end == self.len{   
                break;
            }
        }
        
        std::thread::sleep(std::time::Duration::from_secs(1));
        m.join_and_clear().unwrap();
        std::thread::sleep(std::time::Duration::from_secs(1));
        threadpool.join();
        let pro_bar = ProgressBar::new(self.len);
        pro_bar.set_style(sty.clone());
        self.downloaded();
    }
}

fn download_at(url: &str, dst: &str, no: i32, header: &str, pro: &ProgressBar, all_need_size: u64) -> Except<u64> {
    let client = reqwest::Client::builder()
        .danger_accept_invalid_certs(true)
        .timeout(std::time::Duration::from_secs(7))
        .build()?;
    // let mut from_size = 0;
    let _size = header.split("=").last().expect("no last");
    let _proname = header.split("=").next().expect("no = in header");
    let path = format!("{}.{}.{}", dst, _size, no);
    
    let mut fr = _size.split("-").next().expect("size cant not be parse as int").parse::<u64>().expect("u64 error");
    // let start_fr = fr;
    let to = _size.split("-").last().expect("size cant not be parse as int").parse::<u64>().expect("u64 error");
    
    let mut this_file_should_be = to - fr + 1;
    let mut is_end = false;
    if to == all_need_size{
        this_file_should_be -= 1;
        is_end = true;
    }
    if Path::new(&path).exists() {
        let meta = metadata(&path)?;
        let now_size = meta.len();
        if now_size < to - fr +1 {
            if is_end && now_size == to - fr{
                if let Ok(_) = meta.created() {
                    pro.finish_with_message(&format!("[{}] no.{} {} / {}","done".green(),no,&format!("{}", now_size).yellow(),this_file_should_be));
                }
                return Ok(this_file_should_be);
            }
            pro.set_message(
                &format!("block:{} continue with: {} {}-{}", no, fr + meta.len(), fr, to)
                    .bold()
                    .yellow()
                    .to_string(),
            );
            std::thread::sleep(std::time::Duration::from_secs(1));
            let size = if now_size > 0{now_size-1}else{0};
            pro.set_position(size);
            fr += now_size ;
        } else if now_size == to - fr +1 {
            if let Ok(_) = meta.created() {
                pro.finish_with_message(&format!("[{}] no.{} {} / {}","done".green(),no,&format!("{}", now_size).yellow(),this_file_should_be));
            }
            return Ok(this_file_should_be);
        }else{
            let _ = remove_file(&path);
        }
    }
    let mut tmp_file = if Path::new(&path).exists(){
        let f = OpenOptions::new().append(true).open(&path)?;
        // let _ = f.set_len(to - start_fr +1);
        pro.set_message(&format!("{} no.{}", "wait 1 s", no));
        std::thread::sleep(std::time::Duration::from_secs(1));
        // from_size = metadata(&path).expect("no exists to meta").len();
        if fr > to{
            fr = to;
        }
        f
    }else{
        std::fs::File::create(&path)?
    };
    
    if header.contains("=") {
        let mut resp = if is_end{
            if fr > to-1 {
                fr =to-1;
            }
            client
            .get(url)
            .header("Range", &format!("{}={}-{}", _proname, fr, to -1))
            .send()?
        }else{
            client
            .get(url)
            .header("Range", &format!("{}={}-{}", _proname, fr, to))
            .send()?
        };
         
        pro.set_message(&format!("{} no.{} / {}-{}", "ready 1 s ", no, fr, to));
        std::thread::sleep(std::time::Duration::from_secs(1));
        copy_with_progress(&format!("no.{} need {} | head {} ", no, to-fr +1, header).green(), &mut resp, &mut tmp_file, pro, Some((fr, to)))
            .expect("no content to download");
        pro.set_message(&format!("{}", "finished wait 3 s"));
        std::thread::sleep(std::time::Duration::from_secs(3));
    }
    // let name = url.split("/").last().expect("no last");
    let now_size = metadata(&path).expect("meta finished file error ").len();
    // pro_bar.set_message(&format!("done:{} / size:{}", no, now_size));
    pro.finish_with_message(&format!(
        "[{}] no.{} {} / {}",
        "done".green(),
        no,
        &format!("{}", now_size).yellow(),
        this_file_should_be
    ));
    Ok(this_file_should_be)
}

fn new_pro(len: u64) -> ProgressBar {
    let pro_bar = ProgressBar::new(len);
    let sty = ProgressStyle::default_bar()
        .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} {bytes_per_sec} ({eta}) {msg}")
        .progress_chars("#>-");
    pro_bar.set_style(sty.clone());
    pro_bar
}

fn copy_with_progress<R: ?Sized, W: ?Sized>(
    msg: &str,
    reader: &mut R,
    writer: &mut W,
    pro: &ProgressBar,
    fr_to:Option<(u64, u64)>
) -> std::io::Result<u64>
where
    R: Read,
    W: Write,
{
    let mut buf = [0; SIZE];
    let mut written = 0;
    let mut ready_exist = false;
    pro.set_message(msg);
    loop {
        // pro.println(&format!("read wait "));
        let mut len = match reader.read(&mut buf) {
            Ok(0) => {
                // ready_exist = true;
                // 0
                // if buf[0] != 0{
                //     writer.write(&buf[..0]).expect("write error ");
                // }
                return Ok(0);
            },
            Ok(len) => len,
            // Err(ref e ) if e.kind() == io::ErrorKind::Interrupted => continue,
            Err(e) => {
                pro.set_message(&format!("{}", e));
                return Err(e);
            }
        };
        if let Some((ff,tt)) = fr_to{
            if ff + written + len as u64 > tt +1{
                len = (tt + 1 - ff - written) as usize;
                ready_exist = true;
            }
        }

        let _ = writer.write_all(&buf[..len]).expect("write error ");
        // if ws < SIZE {
        //     'some: loop {
        //         ws += writer.write(&buf[ws..len]).expect("write error ");
        //         if ws == len {
        //             break 'some;
        //         }
        //     }
        // }
        // pro.println(&format!("read {}", written));
        // writer.write(&buf[..len]).expect("write error ");
        written += len as u64;
        
        buf = [0; SIZE];
        if ready_exist{
            // std::thread::sleep(std::time::Duration::from_secs(3));
            return Ok(written);
        }
        pro.inc(len as u64);
        
    }
}

pub fn set_tmp_dir(dst: &str) {
    let mut dir = RES.lock().expect("lock failed");
    let real_dst = dst.glob();
    let path = Path::new(&real_dst);
    if !path.exists() {
        match create_dir_all(&path) {
            Ok(_) => {
                info!("craete {} [{}]", path.display(), "ok".green());
            }
            _ => {
                error!("craete {} [{}]", path.display(), "X".green());
            }
        };
    }
    dir.dir = path.to_str().expect("to str error").to_string();
}

fn get_tmp_dir() -> String {
    let tmp_dir = RES.lock().expect("unlock");
    tmp_dir.get_path()
}

pub fn filter_file_from_url(url: &str, type_f: &str) -> Vec<String> {
    let client = reqwest::Client::new();
    let re = Regex::new(r#"(http.+?)""#).unwrap();
    // let rel = Regex::new(r"(,\w{2})").unwrap();
    let ss = Regex::new(r"(\\u002F)").unwrap();
    let mut resp = client.get(url).send().expect("send failed!");
    let buf = resp.text().expect("no text");
    let mut urls_f: Vec<String> = vec![];
    info!("{} by [{}]", "filter all file by ".blue(), type_f.green());
    for cap in re.captures_iter(&buf) {
        let raw_u = format!("{}", &cap[1]);
        let u = ss.replace_all(&raw_u, "/").to_string();
        if u.ends_with(type_f) {
            info!("found file : {}", u.magenta());
            urls_f.push(u);
        }
    }
    urls_f
}
